use super::*;

pub(crate) fn expand(attr: &EnumType, input: &DeriveInput) -> TokenStream {
	let body = match &input.data {
		Data::Struct(data) => impl_struct(attr, input, data),
		Data::Enum(data) => impl_enum(attr, input, data),
		Data::Union(_) => Err(Error::new_spanned(input, "union are not supported")),
	};
	let body = match body {
		Ok(body) => body,
		Err(error) => return error.to_compile_error(),
	};
	let ty = &input.ident;
	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
	quote! {
		#[allow(unused_qualifications)]
		#[automatically_derived]
		impl #impl_generics ::napih::IntoNapi for #ty #ty_generics #where_clause {
			fn into_napi<'__napih_a>(self, env: &'__napih_a ::napih::Env) -> ::napih::Result<'static, ::napih::Value<'__napih_a>> {
				#body
			}
		}
	}
}
fn impl_struct(attr: &EnumType, _: &DeriveInput, data: &DataStruct) -> Result<TokenStream> {
	match attr {
		EnumType::Obj => {
			match &data.fields {
				Fields::Named(FieldsNamed { named: fields, .. }) => {
					if fields.iter().any(|field| !matches!(field.vis, Visibility::Public(_))) {
						return Err(Error::new_spanned(fields, "private fields are not supported for IntoNapi"));
					}
					let field_entries = fields.iter().map(|field| {
						let ident = field.ident.as_ref().expect("field should have name");
						let name = match ident.to_string().lit_cstr() {
							Ok(name) => name,
							Err(error) => return error,
						};
						quote! {
							{
								let value = self.#ident.into_napi(env)?;
								obj.set_named(#name, value)?;
							}
						}
					});
					Ok(quote! {
						let obj = ::napih::Object::new(env)?.into_napi(env)?;
						#(#field_entries)*
						Ok(obj)
					})
				}
				Fields::Unit => {
					Ok(quote! {
						// Unit structs are converted to empty objects
						::napih::Object::new(env)?.into_napi(env)
					})
				}
				_ => Err(Error::new(Span::call_site(), "tuple structs are not supported for IntoNapi")),
			}
		}
		_ => Err(Error::new(Span::call_site(), "unsupport")),
	}
}
fn impl_enum(attr: &EnumType, input: &DeriveInput, data: &DataEnum) -> Result<TokenStream> {
	let ty = &input.ident;
	match attr {
		EnumType::Num => {
			let mut items = vec![];
			for var in data.variants.iter() {
				let name = &var.ident;
				match &var.discriminant {
					Some((_, Expr::Lit(ExprLit { lit: Lit::Int(v), .. }))) => items.push(quote! {#ty::#name => #v.into_napi(env)}),
					_ => return Err(Error::new_spanned(var, "enum variant must have discriminant")),
				}
			}
			Ok(quote! {match self {#(#items,)*}})
		}
		EnumType::Str => {
			let mut items = vec![];
			for var in data.variants.iter() {
				let name = &var.ident;
				let value = LitStr::new(&name.to_string(), Span::call_site());
				items.push(quote! {#ty::#name => #value.into_napi(env)});
			}
			Ok(quote! {match self {#(#items,)*}})
		}
		_ => Err(Error::new(Span::call_site(), "unsupport")),
	}
}
